/*******************************************************************************
 * Copyright (c) 2010 Oak Ridge National Laboratory.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 ******************************************************************************/
package org.csstudio.swt.xygraph.dataprovider;

import java.util.Calendar;
import java.util.Iterator;

import org.csstudio.swt.xygraph.linearscale.Range;
import org.eclipse.swt.widgets.Display;


/**
 * Provides data to a trace. 
 * @author Xihui Chen
 *
 */
public class CircularBufferDataProvider extends AbstractDataProvider{
	
	public enum UpdateMode{
		X_OR_Y("X or Y"),
		X_AND_Y("X AND Y"),
		X("X"),
		Y("Y"),
		TRIGGER("Trigger");
				
		private UpdateMode(String description) {
			 this.description = description;
		}
		private String description;
		
		@Override
		public String toString() {
			return description;
		}
		public static String[] stringValues(){
			String[] sv = new String[values().length];
			int i=0;
			for(UpdateMode p : values())
				sv[i++] = p.toString();
			return sv;
		}
	}	

	public enum PlotMode{
		LAST_N("Plot last n pts."),
		N_STOP("Plot n pts & stop.");	
				
		private PlotMode(String description) {
			 this.description = description;
		}
		private String description;
		
		@Override
		public String toString() {
			return description;
		}
		public static String[] stringValues(){
			String[] sv = new String[values().length];
			int i=0;
			for(PlotMode p : values())
				sv[i++] = p.toString();
			return sv;
		}
	}
	
	private CircularBuffer<ISample> traceData;	
	
	private double currentXData;
	
	private double currentYData;
	
	private long currentYDataTimestamp;	
	
	private boolean currentXDataChanged = false;
	
	private boolean currentYDataChanged = false;
	
	private boolean currentYDataTimestampChanged = false;
	
	private double[] currentXDataArray = new double[]{};
	
	private double[] currentYDataArray = new double[]{};
	
	private boolean currentXDataArrayChanged = false;
	
	private boolean currentYDataArrayChanged = false;
	
	private boolean xAxisDateEnabled = false;
	
	private int updateDelay = 0;
	private boolean duringDelay = false;
	
	private boolean concatenate_data = true;
	
	/**
	 * this indicates if the max and min of the data need to be recalculated.
	 */
	private boolean dataRangedirty = false;
	
	private UpdateMode updateMode = UpdateMode.X_AND_Y;
	
	private PlotMode plotMode = PlotMode.LAST_N;

	private Runnable fireUpdate;
	
	public CircularBufferDataProvider(boolean chronological) {
		super(chronological);
		traceData = new CircularBuffer<ISample>(100);
		fireUpdate = new Runnable(){
			public void run() {
				for(IDataProviderListener listener : listeners){
					listener.dataChanged(CircularBufferDataProvider.this);
				}
				duringDelay = false;
			}			
		};
	}

	/**
	 * @param currentXData the currentXData to set
	 */
	public synchronized void setCurrentXData(double newValue) {
		this.currentXData = newValue;
		currentXDataChanged = true;
		tryToAddDataPoint();
	}


	/**Set current YData.
	 * @param currentYData the currentYData to set
	 */
	public synchronized void setCurrentYData(double newValue) {
		this.currentYData = newValue;
		currentYDataChanged = true;
		if(!xAxisDateEnabled|| (xAxisDateEnabled && currentYDataTimestampChanged))
			tryToAddDataPoint();
	}
	
	public synchronized void addSample(ISample sample){
		if(traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		traceData.add(sample);
		fireDataChange();
	}
	
	/**Set the time stamp of currrent YData
	 * @param timestamp timestamp of Y data in milliseconds.
	 */
	public synchronized void setCurrentYDataTimestamp(long timestamp){
		if(!xAxisDateEnabled){
			clearTrace();
			xAxisDateEnabled = true;
		}		
		this.currentYDataTimestamp = timestamp;
		currentYDataTimestampChanged = true;
		if(currentYDataChanged)
			tryToAddDataPoint();		
	}
	
	/**Set current YData and its timestamp when the new value generated.
	 * @param currentYData the currentYData to set
	 * @param timestamp timestamp of Y data in milliseconds.
	 */
	public synchronized void setCurrentYData(double newValue, long timestamp) {
		xAxisDateEnabled = true;
		this.currentYData = newValue;
		currentYDataChanged = true;
		this.currentYDataTimestamp = timestamp;
		currentYDataTimestampChanged = true;		
		tryToAddDataPoint();
	}
	
	/**
	 * Try to add a new data point to trace data. 
	 * Whether it will be added or not is up to the update mode.
	 */
	private void tryToAddDataPoint(){
		if(traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		switch (updateMode) {
		case X_OR_Y:
			if((chronological && currentYDataChanged) ||
					(!chronological && (currentXDataChanged || currentYDataChanged)))
				addDataPoint();
			break;
		case X_AND_Y:
			if((chronological && currentYDataChanged) ||
					(!chronological && (currentXDataChanged && currentYDataChanged)))
				addDataPoint();					
			break;
		case X:
			if((chronological && currentYDataChanged) || 
					(!chronological && currentXDataChanged))
				addDataPoint();
			break;
		case Y:			
			if(currentYDataChanged)
				addDataPoint();
			break;
		case TRIGGER:

		default:
			break;
		}
	}


	/**
	 * add a new data point to trace data.
	 */
	private void addDataPoint() {	
		double newXValue;		
		if(!concatenate_data)
			traceData.clear();
		if(chronological){
			if(xAxisDateEnabled){
				if(updateMode != UpdateMode.TRIGGER)
					newXValue = currentYDataTimestamp;
				else
					newXValue = Calendar.getInstance().getTimeInMillis();
			}
			else{
				if(traceData.size() == 0)
					newXValue = 0;
				else
					newXValue = traceData.getTail().getXValue() +1;
			}					
		}else{
			newXValue = currentXData;
		}
			traceData.add(new Sample(newXValue, currentYData));
			currentXDataChanged = false;
			currentYDataChanged = false;
			currentYDataTimestampChanged = false;
			fireDataChange();			
	}
	
	
	/**
	 * @param currentXData the currentXData to set
	 */
	public synchronized void setCurrentXDataArray(double[] newValue) {
		this.currentXDataArray = newValue;
		currentXDataArrayChanged = true;
		tryToAddDataArray();
	}
	
	/**
	 * @param currentXData the currentXData to set
	 */
	public synchronized void setCurrentYDataArray(double[] newValue) {
		this.currentYDataArray = newValue;
		currentYDataArrayChanged = true;
		tryToAddDataArray();
	}
	
	
	/**
	 * Try to add a new data array to trace data. 
	 * Whether it will be added or not is up to the update mode.
	 */
	private void tryToAddDataArray(){
		if(traceData.size() == traceData.getBufferSize() && plotMode == PlotMode.N_STOP)
			return;
		switch (updateMode) {
		case X_OR_Y:
			if((chronological && currentYDataArrayChanged) ||
					(!chronological && (currentXDataArrayChanged || currentYDataArrayChanged)))
				addDataArray();
			break;
		case X_AND_Y:
			if((chronological && currentYDataArrayChanged) ||
					(!chronological && (currentXDataArrayChanged && currentYDataArrayChanged)))
				addDataArray();					
			break;
		case X:
			if((chronological && currentYDataArrayChanged) || 
					(!chronological && currentXDataArrayChanged))
				addDataArray();
			break;
		case Y:			
			if(currentYDataArrayChanged)
				addDataArray();
			break;	
		case TRIGGER:
		default:
			break;
		}
	}
	
	
	/**
	 * add a new data point to trace data.
	 */
	private void addDataArray() {	
		if(!concatenate_data)
			traceData.clear();
			
		if(chronological){	
			double[] newXValueArray;
			newXValueArray = new double[currentYDataArray.length];
			if(traceData.size() == 0)
				for(int i=0; i<currentYDataArray.length; i++){
					newXValueArray[i] = i;
				}
			else
				for(int i=1; i<currentYDataArray.length+1; i++){
					newXValueArray[i-1] = traceData.getTail().getXValue() + i;
				}
			for(int i=0; i<Math.min(newXValueArray.length, currentYDataArray.length); i++){
				traceData.add(new Sample(newXValueArray[i], currentYDataArray[i]));
			}					
		}else{			
			//newXValueArray = currentXDataArray;
			
			// if the data array size is longer than buffer size, 
			//just ignore the tail data.
			for(int i=0; i<Math.min(traceData.getBufferSize(),
					Math.min(currentXDataArray.length, currentYDataArray.length)); i++){
				traceData.add(new Sample(currentXDataArray[i], currentYDataArray[i]));
			}
		}
		
			currentXDataChanged = false;
			currentYDataChanged = false;
			currentYDataTimestampChanged = false;
			fireDataChange();		
	}
	

	public synchronized void clearTrace(){
		traceData.clear();
		fireDataChange();
	}
	
	public Iterator<ISample> iterator() {
		return traceData.iterator();
	}

	/**
	 * @param bufferSize the bufferSize to set
	 */
	public synchronized void setBufferSize(int bufferSize) {
		traceData.setBufferSize(bufferSize, false);
	}

	
	/**
	 * @param updateMode the updateMode to set
	 */
	public void setUpdateMode(UpdateMode updateMode) {
		this.updateMode = updateMode;
	}
	
	/**
	 * @return the update mode.
	 */
	public UpdateMode getUpdateMode() {
		return updateMode;
	}

	/**In TRIGGER update mode, the trace data could be updated by this method 
	 * @param triggerValue the triggerValue to set
	 */
	public void triggerUpdate() {
		//do not update if no new data was added, otherwise, it will add (0,0) which is not a real sample.
		if(traceData.size() == 0 && !(currentYDataChanged || currentYDataArrayChanged))
			return;
		if(currentYDataArray.length > 0)
			addDataArray();
		else
			addDataPoint();
	}

	@Override
	protected void innerUpdate() {
		dataRangedirty = true;
	}
	
	@Override
    protected void updateDataRange(){
		if(!dataRangedirty)
			return;
		dataRangedirty = false;
		if(getSize() > 0){
			double xMin;
			double xMax;
			xMin = traceData.getHead().getXValue();
			xMax = xMin;
			
			double yMin;
			double yMax;
			yMin = traceData.getHead().getYValue();
			yMax = yMin;
			for(ISample dp : traceData){
				if(xMin > dp.getXValue()-dp.getXMinusError())
					xMin = dp.getXValue()-dp.getXMinusError();
				if(xMax < dp.getXValue()+dp.getXPlusError())
					xMax = dp.getXValue()+ dp.getXPlusError();	
				
				if(yMin > dp.getYValue() - dp.getYMinusError())
					yMin = dp.getYValue() - dp.getYMinusError();
				if(yMax < dp.getYValue() + dp.getYPlusError())
					yMax = dp.getYValue() + dp.getYPlusError();	
			}
			
			xDataMinMax = new Range(xMin, xMax);
			yDataMinMax = new Range(yMin, yMax);
		}else {
			xDataMinMax = null;
			yDataMinMax = null;
		}
	}

	/**
	 * @param plotMode the plotMode to set
	 */
	public void setPlotMode(PlotMode plotMode) {
		this.plotMode = plotMode;
	}

	@Override
	public ISample getSample(int index) {
		return traceData.getElement(index);
	}

	@Override
	public int getSize() {
		return traceData.size();
	}

	/**If xAxisDateEnable is true, you will need to use 
	 * {@link #setCurrentYData(double, long)} or {@link #setCurrentYDataTimestamp(long)} to set the 
	 * time stamp of ydata. This flag will be automatically enabled when
	 * either of these two methods were called.
	 * The default value is false.
	 * @param xAxisDateEnabled the xAxisDateEnabled to set
	 */
	public void setXAxisDateEnabled(boolean xAxisDateEnabled) {
		this.xAxisDateEnabled = xAxisDateEnabled;
	}

	/**
	 * @param updateDelay Delay in milliseconds between plot updates. This may help to reduce CPU
	 * usage. The default value is 0ms.
	 */
	public synchronized void setUpdateDelay(int updateDelay) {
		this.updateDelay = updateDelay;
	}
	
	@Override
	protected synchronized void fireDataChange() {
		if(updateDelay >0){
			innerUpdate();			
			if(!duringDelay){
				Display.getCurrent().timerExec(updateDelay, fireUpdate);	
				duringDelay = true;
			}					
		}else
			super.fireDataChange();			
	}

	public void setConcatenate_data(boolean concatenate_data) {
		this.concatenate_data = concatenate_data;
	}

	public boolean isConcatenate_data() {
		return concatenate_data;
	}
	

}
